import dash
from dash import dcc, html, Input, Output, callback
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import dash_bootstrap_components as dbc

# 设置随机种子确保可复现
np.random.seed(42)


# ======================
# 模拟数据生成
# ======================

def generate_sales_data():
    """生成销售业绩数据"""
    dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='ME')  # 修改：M -> ME
    new_business = np.random.randint(500, 2000, size=12) * 10000
    renewal = np.random.randint(300, 1500, size=12) * 10000
    targets = (new_business * 1.2).astype(int)

    channels = ['代理人', '线上平台', '银保渠道', '直销']
    channel_share =  [0.45, 0.30, 0.20, 0.05]
    avg_premium =  [8200, 3500, 15000, 6800]
    yoy_growth =  [0.12, 0.25, -0.05, 0.03]

    products =  ['车险', '健康险', '寿险', '财产险', '意外险']
    product_dist =  [0.35, 0.25, 0.20, 0.15, 0.05]

    return {
        'dates': dates,
        'new_business': new_business,
        'renewal': renewal,
        'targets': targets,
        'channels': channels,
        'channel_share': channel_share,
        'avg_premium': avg_premium,
        'yoy_growth': yoy_growth,
        'products': products,
        'product_dist': product_dist
    }


def generate_risk_data():
    """生成风险与理赔数据"""
    regions = ['华东', '华北', '华南', '华中', '西南', '西北', '东北']
    products = ['车险', '健康险', '寿险', '财产险']

    # 地区-产品赔付率热力图
    claim_rates = np.random.uniform(0.4, 0.95, size=(len(regions), len(products)))

    # 月度理赔数据
    months = pd.date_range('2024-01-01', periods=12, freq='ME')  # 修改：M -> ME
    claim_amounts = np.random.randint(200, 800, 12) * 10000
    avg_claim = np.random.randint(8000, 25000, 12)

    # 高赔案件
    claim_ids =  [f'CL2024-{str(i).zfill(3)}' for i in range(1, 11)]
    claim_amounts_top = np.random.randint(100000, 600000, 10)
    claim_reasons =  ['交通事故', '恶性肿瘤', '自然灾害', '心脑血管疾病', '火灾',
                     '意外坠落', '猝死', '水灾', '盗窃', '手术并发症']

    return {
        'regions': regions,
        'risk_products': products,
        'claim_rates': claim_rates,
        'months': months,
        'claim_amounts': claim_amounts,
        'avg_claim': avg_claim,
        'claim_ids': claim_ids,
        'claim_amounts_top': claim_amounts_top,
        'claim_reasons': claim_reasons
    }


def generate_customer_data():
    """生成客户洞察数据"""
    segments = ['高端客户', '中端客户', '大众客户']
    segment_share = [0.05, 0.20, 0.75]
    segment_color = ['#FFD700', '#C0C0C0', '#CD7F32']

    # 续保概率
    renewal_prob = np.random.uniform(0.45, 0.85)

    # 新客来源桑基图数据
    sources =  ['线上广告', '代理人推荐', '银行合作', '老客转介']
    products =  ['车险', '健康险', '寿险', '财产险']
    age_groups =  ['<30岁', '30-45岁', '45-60岁', '>60岁']

    source_to_product = np.random.randint(50, 200, size=(len(sources), len(products)))
    product_to_age = np.random.randint(30, 150, size=(len(products), len(age_groups)))

    return {
        'segments': segments,
        'segment_share': segment_share,
        'segment_color': segment_color,
        'renewal_prob': renewal_prob,
        'sources': sources,
        'sankey_products': products,
        'age_groups': age_groups,
        'source_to_product': source_to_product,
        'product_to_age': product_to_age
    }


def generate_geo_data():
    """生成地理分析数据"""
    provinces = ['广东', '江苏', '山东', '浙江', '河南', '四川', '河北', '福建', '湖北', '湖南']
    premium = np.random.randint(500, 2500, len(provinces)) * 10000
    claim_ratio = np.random.uniform(0.4, 0.85, len(provinces))
    claim_amount = np.random.randint(100, 500, len(provinces)) * 10000

    return {
        'provinces': provinces,
        'premium': premium,
        'claim_ratio': claim_ratio,
        'claim_amount': claim_amount
    }


# ======================
# 数据准备
# ======================
sales_data = generate_sales_data()
risk_data = generate_risk_data()
customer_data = generate_customer_data()
geo_data = generate_geo_data()

# 当前月份KPI计算
current_month = sales_data['dates'][-1]
total_premium = sales_data['new_business'][-1] + sales_data['renewal'][-1]
new_business_premium = sales_data['new_business'][-1]
renewal_rate = round(sales_data['renewal'][-1] / (sales_data['renewal'][-1] + sales_data['new_business'][-1] * 0.3), 2)
claim_ratio = round(risk_data['claim_rates'].mean(), 2)
persistency_rate = round(np.random.uniform(0.85, 0.95), 2)

# ======================
# Dash应用初始化
# ======================
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
app.title = '保险业务智能看板'

# ======================
# 看板布局
# ======================
app.layout = dbc.Container([
    # 标题行
    dbc.Row([
        dbc.Col(html.H1("保险业务智能看板",
                        className="text-center my-4",
                        style={'color': '#003366', 'fontWeight': 'bold'}),
                width=12)
    ]),

    # KPI概览卡片
    dbc.Row([
        dbc.Col(dbc.Card([
            dbc.CardHeader("总保费收入", className="text-center"),
            dbc.CardBody([
                html.H4(f"¥{total_premium / 10000:.1f}万", className="card-title text-center"),
                html.P(f"目标达成: {total_premium / sales_data['targets'][-1] * 100:.1f}%",
                       className=f"card-text text-center {'text-danger' if total_premium < sales_data['targets'][-1] else 'text-success'}")
            ])
        ], color="light", outline=True), md=2),

        dbc.Col(dbc.Card([
            dbc.CardHeader("新单保费", className="text-center"),
            dbc.CardBody([
                html.H4(f"¥{new_business_premium / 10000:.1f}万", className="card-title text-center"),
                html.P(f"环比: +{np.random.randint(5, 15)}%",
                       className="card-text text-center text-success")
            ])
        ], color="light", outline=True), md=2),

        dbc.Col(dbc.Card([
            dbc.CardHeader("续保率", className="text-center"),
            dbc.CardBody([
                html.H4(f"{renewal_rate * 100:.1f}%", className="card-title text-center"),
                html.P(f"行业基准: 72.5%",
                       className=f"card-text text-center {'text-danger' if renewal_rate < 0.725 else 'text-success'}")
            ])
        ], color="light", outline=True), md=2),

        dbc.Col(dbc.Card([
            dbc.CardHeader("综合赔付率", className="text-center"),
            dbc.CardBody([
                html.H4(f"{claim_ratio * 100:.1f}%", className="card-title text-center"),
                html.P("警戒线: 75%",
                       className=f"card-text text-center {'text-danger' if claim_ratio > 0.75 else 'text-success'}")
            ])
        ], color="light", outline=True), md=2),

        dbc.Col(dbc.Card([
            dbc.CardHeader("保单继续率", className="text-center"),
            dbc.CardBody([
                html.H4(f"{persistency_rate * 100:.1f}%", className="card-title text-center"),
                html.P("趋势: 稳定", className="card-text text-center text-success")
            ])
        ], color="light", outline=True), md=2),

        dbc.Col(dbc.Card([
            dbc.CardHeader("数据日期", className="text-center"),
            dbc.CardBody([
                html.H4(current_month.strftime("%Y-%m"), className="card-title text-center"),
                html.P("每日更新", className="card-text text-center text-muted")
            ])
        ], color="light", outline=True), md=2),
    ], className="mb-4"),

    # 第一行图表
    dbc.Row([
        # 保费趋势图
        dbc.Col(dcc.Graph(id='premium-trend-chart'), md=6),

        # 渠道贡献分析
        dbc.Col(dcc.Graph(id='channel-analysis-chart'), md=3),

        # 产品线分布
        dbc.Col(dcc.Graph(id='product-distribution-chart'), md=3),
    ], className="mb-4"),

    # 第二行图表
    dbc.Row([
        # 地区-产品赔付率热力图
        dbc.Col(dcc.Graph(id='claim-heatmap-chart'), md=5),

        # 理赔趋势
        dbc.Col(dcc.Graph(id='claim-trend-chart'), md=4),

        # 客户分层
        dbc.Col(dcc.Graph(id='customer-segmentation-chart'), md=3),
    ], className="mb-4"),

    # 第三行图表
    dbc.Row([
        # 地理分析
        dbc.Col(dcc.Graph(id='geo-analysis-chart'), md=6),

        # 续保预警
        dbc.Col(dcc.Graph(id='renewal-warning-chart'), md=3),

        # 高赔案件
        dbc.Col(dcc.Graph(id='high-claim-cases'), md=3),
    ], className="mb-4"),

    # 新客来源桑基图
    dbc.Row([
        dbc.Col(dcc.Graph(id='customer-journey-chart'), md=12)
    ])
], fluid=True)


# ======================
# 图表生成函数
# ======================

@app.callback(
    Output('premium-trend-chart', 'figure'),
    Input('premium-trend-chart', 'id')
)
def update_premium_trend(_):
    fig = go.Figure()

    # 添加新单保费柱状图
    fig.add_trace(go.Bar(
        x=sales_data['dates'],
        y=sales_data['new_business'],
        name='新单保费',
        marker_color='#1f77b4'
    ))

    # 添加续期保费柱状图
    fig.add_trace(go.Bar(
        x=sales_data['dates'],
        y=sales_data['renewal'],
        name='续期保费',
        marker_color='#ff7f0e'
    ))

    # 添加达成率折线图（次Y轴）
    achievement_rate = [(n + r) / t for n, r, t in zip(sales_data['new_business'],
                                                       sales_data['renewal'],
                                                       sales_data['targets'])]
    fig.add_trace(go.Scatter(
        x=sales_data['dates'],
        y=achievement_rate,
        name='达成率',
        yaxis='y2',
        mode='lines+markers',
        line=dict(color='#d62728', width=3)
    ))

    fig.update_layout(
        title='月度保费趋势分析',
        barmode='stack',
        yaxis=dict(title='保费金额 (元)'),
        yaxis2=dict(
            title='达成率 (%)',
            overlaying='y',
            side='right',
            range=[0, max(achievement_rate) * 1.2],
            tickformat='.0%'
        ),
        legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1),
        plot_bgcolor='rgba(240,240,240,0.8)'
    )
    return fig


@app.callback(
    Output('channel-analysis-chart', 'figure'),
    Input('channel-analysis-chart', 'id')
)
def update_channel_analysis(_):
    # 环形图
    fig = go.Figure()
    fig.add_trace(go.Pie(
        labels=sales_data['channels'],
        values=sales_data['channel_share'],
        hole=0.5,
        marker_colors=['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728'],
        textinfo='percent+label',
        hoverinfo='label+value'
    ))

    fig.update_layout(
        title='渠道保费贡献',
        annotations=[dict(text='渠道占比', x=0.5, y=0.5, font_size=14, showarrow=False)]
    )
    return fig


@app.callback(
    Output('product-distribution-chart', 'figure'),
    Input('product-distribution-chart', 'id')
)
def update_product_distribution(_):
    # 按保费排序
    sorted_idx = np.argsort(sales_data['product_dist'])[::-1]  # 修正：使用销售数据中的产品分布
    sorted_products = [sales_data['products'][i] for i in sorted_idx]
    sorted_dist = [sales_data['product_dist'][i] for i in sorted_idx]

    fig = go.Figure(go.Bar(
        y=sorted_products,
        x=sorted_dist,
        orientation='h',
        marker_color=['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
    ))

    fig.update_layout(
        title='产品线保费分布',
        xaxis=dict(title='保费占比'),
        yaxis=dict(autorange="reversed"),
        height=300
    )
    return fig


@app.callback(
    Output('claim-heatmap-chart', 'figure'),
    Input('claim-heatmap-chart', 'id')
)
def update_claim_heatmap(_):
    fig = go.Figure(data=go.Heatmap(
        z=risk_data['claim_rates'],
        x=risk_data['risk_products'],
        y=risk_data['regions'],
        colorscale='Reds',
        hoverongaps=False,
        zmin=0.4,
        zmax=0.95,
        colorbar=dict(title='赔付率')
    ))

    fig.update_layout(
        title='地区-产品线赔付率热力图',
        xaxis=dict(title='产品类型'),
        yaxis=dict(title='地区'),
        height=350
    )
    return fig


@app.callback(
    Output('claim-trend-chart', 'figure'),
    Input('claim-trend-chart', 'id')
)
def update_claim_trend(_):
    fig = go.Figure()

    # 理赔金额柱状图
    fig.add_trace(go.Bar(
        x=risk_data['months'],
        y=risk_data['claim_amounts'],
        name='理赔金额',
        marker_color='#1f77b4'
    ))

    # 案均赔付折线图（次Y轴）
    fig.add_trace(go.Scatter(
        x=risk_data['months'],
        y=risk_data['avg_claim'],
        name='案均赔付',
        yaxis='y2',
        mode='lines+markers',
        line=dict(color='#d62728', width=3)
    ))

    fig.update_layout(
        title='月度理赔趋势',
        yaxis=dict(title='理赔金额 (元)'),
        yaxis2=dict(
            title='案均赔付 (元)',
            overlaying='y',
            side='right'
        ),
        legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1),
        height=350
    )
    return fig


@app.callback(
    Output('customer-segmentation-chart', 'figure'),
    Input('customer-segmentation-chart', 'id')
)
def update_customer_segmentation(_):
    fig = go.Figure(go.Funnelarea(
        values=customer_data['segment_share'],
        text=customer_data['segments'],
        marker=dict(colors=customer_data['segment_color']),
        textinfo="text+percent",
        hoverinfo="value+text"
    ))

    fig.update_layout(
        title='客户价值分层',
        showlegend=False,
        height=350
    )
    return fig


@app.callback(
    Output('geo-analysis-chart', 'figure'),
    Input('geo-analysis-chart', 'id')
)
def update_geo_analysis(_):
    fig = go.Figure()

    # 气泡图：气泡大小=赔付金额，颜色=赔付率
    fig.add_trace(go.Scattergeo(
        locationmode='china',
        locations=geo_data['provinces'],
        text=geo_data['provinces'],
        marker=dict(
            size=geo_data['claim_amount'] / 5000,
            color=geo_data['claim_ratio'],
            colorscale='Reds',
            cmin=0.4,
            cmax=0.85,
            colorbar=dict(title='赔付率'),
            line=dict(width=0.5, color='grey')
        ),
        name='赔付分析'
    ))

    fig.update_geos(
        scope='asia',
        center=dict(lon=105, lat=36),
        projection_scale=5,
        showcountries=False,
        showcoastlines=False,
        showland=False,
        fitbounds="locations"
    )

    fig.update_layout(
        title='区域保费与赔付分析',
        geo=dict(
            bgcolor='rgba(240,240,240,0.8)',
            lakecolor='rgb(210, 230, 255)',
            landcolor='rgb(250, 250, 250)',
            subunitcolor='grey'
        ),
        height=400
    )
    return fig


@app.callback(
    Output('renewal-warning-chart', 'figure'),
    Input('renewal-warning-chart', 'id')
)
def update_renewal_warning(_):
    fig = go.Figure(go.Indicator(
        mode="gauge+number",
        value=customer_data['renewal_prob'] * 100,
        number={'suffix': '%'},
        domain={'x': [0, 1], 'y': [0, 1]},
        title={'text': "30天内到期客户续保概率"},
        gauge={
            'axis': {'range': [0, 100]},
            'bar': {'color': '#d62728' if customer_data['renewal_prob'] < 0.6 else '#2ca02c'},
            'steps': [
                {'range': [0, 60], 'color': 'lightgray'},
                {'range': [60, 80], 'color': 'gray'},
                {'range': [80, 100], 'color': 'darkgray'}],
            'threshold': {
                'line': {'color': "red", 'width': 4},
                'thickness': 0.75,
                'value': 60}
        }
    ))

    fig.update_layout(height=300)
    return fig


@app.callback(
    Output('high-claim-cases', 'figure'),
    Input('high-claim-cases', 'id')
)
def update_high_claim_cases(_):
    # 按赔付金额排序
    sorted_idx = np.argsort(risk_data['claim_amounts_top'])[::-1]
    claim_ids = [risk_data['claim_ids'][i] for i in sorted_idx]
    amounts = [risk_data['claim_amounts_top'][i] for i in sorted_idx]
    reasons = [risk_data['claim_reasons'][i] for i in sorted_idx]

    fig = go.Figure(go.Bar(
        x=amounts,
        y=claim_ids,
        orientation='h',
        marker_color='#d62728',
        text=[f"¥{a / 10000:.1f}万" for a in amounts],
        textposition='auto',
        hovertext=reasons
    ))

    fig.update_layout(
        title='TOP 10高赔案件',
        xaxis=dict(title='赔付金额'),
        yaxis=dict(autorange="reversed", title='案件ID'),
        height=300
    )
    return fig


@app.callback(
    Output('customer-journey-chart', 'figure'),
    Input('customer-journey-chart', 'id')
)
def update_customer_journey(_):
    # 构建桑基图数据
    labels = customer_data['sources'] + customer_data['sankey_products'] + customer_data['age_groups']

    # 源节点索引
    source_indices = []
    for i in range(len(customer_data['sources'])):
        source_indices.extend([i] * len(customer_data['sankey_products']))

    # 目标节点索引（产品层）
    target_indices = []
    for i in range(len(customer_data['sources'])):
        target_indices.extend([j + len(customer_data['sources']) for j in range(len(customer_data['sankey_products']))])

    # 产品到年龄层的连接
    for i in range(len(customer_data['sankey_products'])):
        source_indices.extend([i + len(customer_data['sources'])] * len(customer_data['age_groups']))
        target_indices.extend([j + len(customer_data['sources']) + len(customer_data['sankey_products']) for j in
                               range(len(customer_data['age_groups']))])

    # 值
    values = customer_data['source_to_product'].flatten().tolist() + customer_data['product_to_age'].flatten().tolist()

    # 颜色
    colors = [
        '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728',  # 来源
        '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728',  # 产品
        '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22'  # 年龄
    ]

    fig = go.Figure(go.Sankey(
        node=dict(
            pad=15,
            thickness=20,
            line=dict(color="black", width=0.5),
            label=labels,
            color=colors
        ),
        link=dict(
            source=source_indices,
            target=target_indices,
            value=values,
            color='rgba(150, 150, 150, 0.4)'
        )
    ))

    fig.update_layout(
        title='新客来源与转化路径分析',
        height=500,
        font_size=12
    )
    return fig


# ======================
# 启动应用
# ======================
if __name__ == '__main__':
    app.run(debug=True, port=8050)  # 修改：run_server -> run